package com.tcl.zenglx.file.service.impl;

import com.google.common.collect.Sets;
import com.tcl.zenglx.file.config.MinIoClientConfig;
import com.tcl.zenglx.file.param.MultipartFileParam;
import com.tcl.zenglx.file.service.StorageService;
import com.tcl.zenglx.file.utils.Constants;
import com.tcl.zenglx.file.utils.FileMD5Util;
import io.minio.*;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author liangxi.zeng
 */
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    /**
     * 保存文件的根目录
     */
    private Path rootPath;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${breakpoint.upload.chunkSize}")
    private long CHUNK_SIZE;

    @Value("${breakpoint.upload.dir}")
    private String finalDirPath;

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinIoClientConfig minIoClientConfig;

    @Autowired
    public StorageServiceImpl(@Value("${breakpoint.upload.dir}") String location) {
        this.rootPath = Paths.get(location);
    }

    @Override
    public void deleteAll() {
        log.info("开发初始化清理数据，start");
        FileSystemUtils.deleteRecursively(rootPath.toFile());
        stringRedisTemplate.delete(Constants.FILE_STATUS_KEY);
        stringRedisTemplate.delete(Constants.FILE_MD5_KEY);
        log.info("开发初始化清理数据，end");
    }

    @Override
    public void init() {
        try {
            if(!rootPath.toFile().exists()) {
                rootPath.toFile().mkdirs();
            } else {
                Files.createDirectory(rootPath);
            }
        } catch (FileAlreadyExistsException e) {
            log.error("文件夹已经存在了，不用再创建。",e);
        } catch (IOException e) {
            log.error("初始化root文件夹失败。", e);
        }
    }


    /**
     * 是否上传完成
     * @param md5
     * @param totalPieces
     * @return
     * @throws Exception
     */
    private boolean isUploadComplete(String md5,int totalPieces) throws Exception{
        Iterable<Result<Item>> results = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(minIoClientConfig.getTmpBucketName())
                        .prefix(md5.concat("/")).build());
        Set<String> objectNames = Sets.newHashSet();
        for (Result<Item> item : results) {
            objectNames.add(item.get().objectName());
        }
        return objectNames.size()==totalPieces;
    }

    /**
     * 上传
     *
     * @return 0~n 前端上传对应分片序号
     * -1 上传完成
     * -2 文件md5对不上
     */
    @Override
    public void uploadFileToMinio(MultipartFileParam param) throws Exception {
        String md5 = param.getMd5();
        //总分片数
        int totalPieces = param.getChunks();
        //当前分片
        int sliceIndex = param.getChunk();
        // 可以最后一个分片传文件名用来保存
        String fileName = param.getName();
        //上传文件
        MultipartFile file = param.getFile();
        // 上传
        minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(minIoClientConfig.getTmpBucketName())
                        .object(md5.concat("/").concat(Integer.toString(sliceIndex)))
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
        //判断是否全部上传完成
        if(isUploadComplete(md5,totalPieces)) {
            log.info("上传临时目录完成--------------");
            //合并文件
            mergeFile(totalPieces,md5,fileName);
            // 删除所有的分片文件
            deleteTempFile(totalPieces,md5);
            log.info("完成上传");
        }
    }

    /**
     * 删除临时文件
     * @param totalPieces
     * @param md5
     */
    private void deleteTempFile(int totalPieces,String md5) {
        List<DeleteObject> delObjects = Stream.iterate(0, i -> ++i)
                .limit(totalPieces)
                .map(i -> new DeleteObject(md5.concat("/").concat(Integer.toString(i))))
                .collect(Collectors.toList());
        log.info("删除临时分片:{}",delObjects);
        minioClient.removeObjects(
                RemoveObjectsArgs.builder().bucket(minIoClientConfig.getTmpBucketName())
                        .objects(delObjects).build());
    }

    /**
     * 合并文件
     * @param totalPieces
     * @param md5
     * @param fileName
     * @throws Exception
     */
    private void mergeFile(int totalPieces,String md5,String fileName) throws Exception{
        // 完成上传从缓存目录合并迁移到正式目录
        List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i)
                .limit(totalPieces)
                .map(i -> ComposeSource.builder()
                        .bucket(minIoClientConfig.getTmpBucketName())
                        .object(md5.concat("/").concat(Integer.toString(i)))
                        .build())
                .collect(Collectors.toList());

        minioClient.composeObject(
                ComposeObjectArgs.builder()
                        .bucket(minIoClientConfig.getBucketName())
                        .object(fileName)
                        .sources(sourceObjectList)
                        .build());
    }


    @Override
    public void uploadFileToLocal(MultipartFileParam param) throws IOException {
        log.info("当前分片：{}，总分片:{}",param.getChunk(),param.getChunks());
        String fileName = param.getName();
        String uploadDirPath = finalDirPath + param.getMd5();
        String tempFileName = fileName;
        File tmpDir = new File(uploadDirPath);
        File tmpFile = new File(uploadDirPath, tempFileName);
        if (!tmpDir.exists()) {
            tmpDir.mkdirs();
        }

        RandomAccessFile tempRaf = new RandomAccessFile(tmpFile, "rw");
        FileChannel fileChannel = tempRaf.getChannel();

        //写入该分片数据
        long offset = CHUNK_SIZE * param.getChunk();
        byte[] fileData = param.getFile().getBytes();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
        mappedByteBuffer.put(fileData);
        // 释放
        FileMD5Util.freedMappedByteBuffer(mappedByteBuffer);
        fileChannel.close();

        boolean isOk = checkAndSetUploadProgress(param,uploadDirPath);
        if (isOk) {
            boolean flag = renameFile(tmpFile, fileName);
            System.out.println("upload complete !!" + flag + " name=" + fileName);
        }
    }

    /**
     * 检查并修改文件上传进度
     *
     * @param param
     * @return
     * @throws IOException
     */
    private boolean checkAndSetUploadProgress(MultipartFileParam param,String uploadDirPath)
            throws IOException {
        String fileName = param.getName();
        File confFile = new File(uploadDirPath, fileName + ".conf");
        RandomAccessFile accessConfFile = new RandomAccessFile(confFile, "rw");
        //把该分段标记为 true 表示完成
        System.out.println("set part " + param.getChunk() + " complete");
        accessConfFile.setLength(param.getChunks());
        accessConfFile.seek(param.getChunk());
        accessConfFile.write(Byte.MAX_VALUE);

        //completeList 检查是否全部完成,如果数组里是否全部都是(全部分片都成功上传)
        byte[] completeList = FileUtils.readFileToByteArray(confFile);
        byte isComplete = Byte.MAX_VALUE;
        for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
            //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
            isComplete = (byte) (isComplete & completeList[i]);
            System.out.println("check part " + i + " complete?:" + completeList[i]);
        }

        accessConfFile.close();
        if (isComplete == Byte.MAX_VALUE) {
            stringRedisTemplate.opsForHash().put(Constants.FILE_STATUS_KEY, param.getMd5(), "true");
            stringRedisTemplate.opsForValue().set(Constants.FILE_MD5_KEY + param.getMd5(), uploadDirPath + "/" + fileName);
            return true;
        } else {
            if (!stringRedisTemplate.opsForHash().hasKey(Constants.FILE_STATUS_KEY, param.getMd5())) {
                stringRedisTemplate.opsForHash()
                        .put(Constants.FILE_STATUS_KEY, param.getMd5(), "false");
            }
            if (stringRedisTemplate.hasKey(Constants.FILE_MD5_KEY + param.getMd5())) {
                stringRedisTemplate.opsForValue()
                        .set(Constants.FILE_MD5_KEY + param.getMd5(), uploadDirPath + "/" + fileName + ".conf");
            }
            return false;
        }
    }

    /**
     * 文件重命名
     *
     * @param toBeRenamed   将要修改名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    public boolean renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的文件是否存在，是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            log.info("File does not exist: " + toBeRenamed.getName());
            return false;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
        return toBeRenamed.renameTo(newFile);
    }

}
